#!/usr/bin/python3

"""
pagamiago - free software script to send PayPal payments

Copyright (C) 2018 Alyssa Rosenzweig

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""

import math
import urllib.parse
import json
import re
import sys
import requests
import os
import codecs
import argparse
import time
import getpass
import random

from lxml.html import parse
from io import StringIO
import subprocess

parser = argparse.ArgumentParser()

parser.add_argument("username")
parser.add_argument("--password")
parser.add_argument("--url")
parser.add_argument("--recipient")
parser.add_argument("--amount")
parser.add_argument("--mock", action="store_true")
parser.add_argument("--debug", action="store_true")
parser.add_argument("--base", default="https://www.sandbox.paypal.com")
parser.add_argument("--country", default="US")
parser.add_argument("--lang", default="en")

args = parser.parse_args()

session = requests.Session()

LOCALE = args.lang + "_" + args.country

LAST_REFERER = "/us/home"
LAST_HEADER_CSRF = None
LAST_CSRF_TOKEN = None

def extract_base_url(url):
    parts = urllib.parse.urlparse(url)
    return parts.scheme + "://" + parts.netloc

def extract_token_from_url(url):
    query_string = urllib.parse.urlparse(url).query
    query = urllib.parse.parse_qs(query_string)
    return query["token"][0]

def mini_dumps(data):
    if isinstance(data, dict):
        return json.dumps(data, separators=(",", ":"))
    else:
        return data

def do_request(url, is_post, data, mock_resp = None, plain_headers = False, like_form = False):
    global LAST_HEADER_CSRF
    global LAST_CSRF_TOKEN

    url = args.base + url

    # Construct data, both for mock and for requests

    if like_form or not is_post:
        print_data = urllib.parse.urlencode({ k:mini_dumps(v) for (k, v) in data.items() })

        if not is_post:
            url = url + (("?" + print_data) if (len(print_data) > 0) else "")
    else:
        print_data = mini_dumps(data)

    # Construct headers

    headers = {
            "Referer": args.base + LAST_REFERER,
            "User-Agent": "Mozilla/5.0 (Linux; Android 7.0; SM-G892A Build/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.107 Mobile Safari/537.36"
    }

    if not plain_headers:
        headers["X-Requested-With"] = "XMLHttpRequest"
        headers["x-csrf-jwt"] = LAST_HEADER_CSRF
        headers["X-CSRF-Token"] = LAST_CSRF_TOKEN

    # Log request if needed

    if args.debug:
        print(("POST" if is_post else "GET") + " " + url)

        if is_post:
            print("")
            print(print_data)

        print("")
 
    # Actuate request
    resp = ""
    
    # Gaussian sleep to be polite
    if not args.mock:
        time.sleep(abs(random.gauss(2, 1)))

    if args.mock:
        if mock_resp is not None:
            with open(mock_resp, "rb") as f:
                resp = f.read().decode("utf-8")
    else:
        if is_post:
            if like_form:
                r = session.post(url, data = data, headers = headers)
            else:
                r = session.post(url, json = data, headers = headers)
        else:
            r = session.get(url, headers = headers)

        if r.status_code >= 400 and r.status_code < 500:
            print(r.text)
            sys.exit(1)

        LAST_HEADER_CSRF = r.headers.get("x-csrf-jwt")

        resp = r.text

    # Parse as JSON if applicable
    return json.loads(resp) if (len(resp) and resp[0] == "{") else resp

def grep_json_string(html, field):
    return re.search('"' + field + '": *"(.*?)"', html).group(1) 

# Creates POSTable object from HTML form

def form_fill(form):
    p = {}

    for (k, v) in form.form_values():
        p[k] = v

    return p

# CAPTCHA (NoScript) authentication flow
# We display the image itself using `feh`, a lightweight image viewer
# TODO: Fallback on other methods of display for other platforms

def login_with_captcha(page):
    tree = parse(StringIO(page)).getroot()

    captcha = tree.cssselect(".captcha-image img")

    if (captcha is None) or (len(captcha) == 0):
        print("No captcha -- proceeding directly")
        return

    url = captcha[0].attrib["src"]
    print(url)

    # Important that shell=False to mitigate security issues !
    view = subprocess.Popen(["feh", "-Bwhite", url], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None, close_fds=True, shell=False)

    form = tree.forms[0]
    form.fields["captcha"] = input("Type the letters on-screen: ")

    # CAPTCHA input over; close window
    view.kill()

    # Send it off
    out = do_request("/auth/validatecaptcha", True, form_fill(form), plain_headers = True, like_form = True)

    with open("tst.html", "wb") as f:
        f.write(out.encode("utf8"))

def basic_authentication(username, password):
    global LAST_REFERER

    AUTH_URL = "/signin?country.x=" + args.country + "&locale.x=" + LOCALE

    auth_page = do_request(AUTH_URL, False, {}, mock_resp = "resps/login.html", plain_headers = True)
    LAST_REFERER = AUTH_URL

    resp = do_request(AUTH_URL, True, {
        "_csrf": re.search('_csrf" value="(.*?)"', auth_page).group(1),
        "locale.x": LOCALE,
        "processSignin": "main",
        "fn_sync_data": "fn_sync_data",
        "intent": "signin",
        "ads-client-context": "signin",
        "requestUrl": AUTH_URL,
        "forcePhonePasswordOptIn": "",
        "login_email": username,
        "login_password": password,
        "btnLogin": "Login",
        "splitLoginContext": "inputPassword",
        "splitLoginCookiedFallback": "true"
    }, plain_headers = True, like_form = True, mock_resp="resps/captcha.html")

    # TODO: When captcha?
    login_with_captcha(resp)

def generate_random_hex_bytes(count):
    if args.mock:
        return "0" * count
    else:
        o = codecs.encode(os.urandom(math.ceil(count / 2)), "hex").decode()
        return o[:-1] if (count % 2) else o

def amount_to_json(amount):
    return {
            "currency_code": amount["currency_code"],
            "value": int(float(amount["amount"]) * 100.0)
    }

def reduced_fields(obj, f):
    return { k: obj[k] for k in obj if k in f }

def peer_to_peer_flow():
    global LAST_CSRF_TOKEN
    global LAST_REFERER

    LAST_REFERER = "/myaccount/transfer/send/preview"
    send = do_request("/myaccount/transfer/send", False, {}, plain_headers = True, mock_resp = "resps/send.html")
    LAST_REFERER = "/myaccount/transfer/send"

    LAST_CSRF_TOKEN = grep_json_string(send, "token")

    details = do_request("/myaccount/transfer/recipientDetails", False, {
        "recipient": args.recipient,
        "isPurchase": "false"
    }, mock_resp = "resps/recipientDetails.json")

    fi = do_request("/myaccount/transfer/fi", False, {
        "env": "",
        "recipient": args.recipient,
        "amount": args.amount,
        "shippingAddressId": details["data"]["user"]["addressBook"][0]["id"],
        "currencyCode": "USD",
        "goods": "false",
        "action": "send",
        "themeId": "",
        "flowType": "send",
        "paymentCardId": "",
        "conversionType": "",
        "recipientCountry": ""
    }, mock_resp = "resps/fi.json")

    do_request("/myaccount/transfer/send", True, {
        "env": "",
        "acount": "send",
        "recipient": args.recipient,
        "fundingOptionId": fi["data"]["fundingOptions"][0]["id"],
        "description": "",
        "donationGiftAidCheckbox": "false",
        "flowType": "send"
    }, like_form = True)

def redirect_flow(redirect_url):
    global LAST_REFERER

    extracted_base = extract_base_url(redirect_url)
    token = extract_token_from_url(redirect_url)

    if extracted_base != args.base:
        print("Error: bad base. Got " + extracted_base + " but expected " + args.base)
        sys.exit(1)

    LAST_REFERER = redirect_url[len(args.base):]
    portal = do_request(redirect_url[len(args.base):], False, {}, mock_resp = "resps/portal.html", plain_headers = True)

    # Grep out what we can from the portal

    merchant_id = grep_json_string(portal, "merchantId")
    merchant_country = grep_json_string(portal, "merchantCountry")
    uid = grep_json_string(portal, "buyer_id")
    return_url = grep_json_string(portal, "return_url")

    amounts_text = re.search('"amounts": (.*?\n            })', portal, re.MULTILINE|re.DOTALL).group(1)
    amount_json = json.loads(amounts_text)

    # The nontrivial requests attach metadata; we can now generate that

    meta = {
        "token": token,
        "calc": "",
        "csci": "",
        "locale": {
            "country": args.country,
            "language": args.lang
        },
        "state": "",
        "app_name": "hermesnodeweb"
    }

    def update_meta(state):
        meta["state"] = state
        meta["csci"] = generate_random_hex_bytes(32)
        meta["calc"] = generate_random_hex_bytes(13)

        return meta

    do_request("/webapps/hermes/api/checkout/" + token + "/flowEligibility", False, {
        "localeCountry": args.country,
        "braintreeInegration": "",
        "meta": update_meta("ui_checkout_landing"),
    })

    do_request("/webapps/hermes/api/info/tokenizedWallet/" + token, False, {
        "meta": update_meta("ui_checkout_login"),
    })

    do_request("/webapps/hermes/api/batch/setbuyer", True, {
        "data": {
            "eConsent": {
                "method": "get",
                "uri": "/api/user/eConsent",
                "params": {},
                "dependencies": []
            },
            "memberSegment": {
                "method": "post",
                "uri": "/api/segment/memberSegment",
                "data": {
                    "page": "HERMES_RYI_PAGE",
                    "site": "HERMES_RYI_PAGE",
                    "locale": LOCALE,
                    "puType": "PERSONAL",
                    "visit_id": token,
                    "si": token,
                    "payment_context": {
                        "business_case": "NONE",
                        "business_case_variation": "",
                        "validate_existing_offer": "false",
                        "total_transaction_amount": amount_to_json(amount_json["total"]),
                        "total_shipping_amount": amount_to_json(amount_json["shipping"]),
                        "payment_units": [
                            {
                                "transaction_amount": amount_to_json(amount_json["total"]),
                                "transaction_id": "",
                                "encrypted_transaction_id": "",
                                "activity_id": "",
                                "payee": {
                                    "pu_type": "BUSINESS",
                                    "country_code": merchant_country,
                                    "epu": merchant_id, 
                                    "pu": ""
                                }
                            }
                        ]
                    },
                    "target_placements": [
                        {
                            "placement_code": "210005",
                            "presentation_types": [
                                "decision"
                            ]
                        }
                    ]
                },
                "params": {},
                "dependencies": []
            },
            "createCheckoutSession": {
                "method": "post",
                "uri": "/api/checkout/" + token + "/session/create",
                "data": {},
                "params": {},
                "dependencies": [
                    "eConsent"
                ]
            }
        },
        "meta": meta,
    })

    do_request("/webapps/hermes/api/checkout/" + token + "/session/create", True, {
        "data": {},
        "meta": meta,
    })

    do_request("/webapps/hermes/api/pxp/xo_hermes_choice_variant", False, {
        "meta": update_meta("ui_checkout_review")
    })

    do_request("/webapps/hermes/api/pxp/xo_hermes_use_cep_credit_offer", False, {
        "uid": uid,
        "ctx.COUNTRY": args.country,
        "meta": meta,
    })

    do_request("/webapps/hermes/api/pxp/xo_mors_hermes_placements_ryi", False, {
        "ctx.COUNTRY": args.country,
        "meta": meta,
    })


    do_request("/webapps/hermes/api/pxp/xo_hermesnodeweb_us_ppc_asset_lite_ramp", False, {
        "meta": meta,
    })

    do_request("/webapps/hermes/api/checkout/" + token + "/installmentOptions/", True, {
        "data": {
            "checkoutAppData": {
                "merchant": {
                    "merchant_country": merchant_country,
                }
            },
            "checkoutCart": {
                "purchase": {
                    "amounts": {
                        "total": reduced_fields(amount_json["total"], ["amount", "currency_code"]),
                    }
                }
            },
            "payee": {
                "merchant": {
                    "id": merchant_id
                }
            },
            "isMemberCredit": True
        },
        "meta": meta,
    })


    do_request("/webapps/hermes/api/checkout/" + token + "/session/authorize", True, {
        "data": {},
        "meta": meta,
    })

    print("---")
    print("Navigate to:")
    print(return_url)

# Enable requests logging

if args.debug:
    import http.client as http_client
    http_client.HTTPConnection.debuglevel = 1

def main():
    if not args.password:
        args.password = getpass.getpass()

    basic_authentication(args.username, args.password)

    if not args.mock:
        time.sleep(5)

    if args.recipient and args.amount:
        peer_to_peer_flow()
    elif args.url:
        redirect_flow(args.url)
    else:
        print("Unknown action specified")
